﻿using Microsoft.Xrm.Client;
using Microsoft.Xrm.Client.Services;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Sustainalytics.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using Sustainalytics.DynamicsCRMGateway.Entities;

namespace Sustainalytics.DynamicsCRMGateway
{
    public class DynamicsCrmGateway : IDynamicsCrmGateway
    {
        #region constants
        private const string CrmAccount = "account";
        private const string CrmOffice = "territory";
        private const string CrmOfficeId = "territoryid";
        private const string Office = "office";
        private const string CrmName = "name";
        private const string CrmManager = "systemuser";
        private const string CrmManagerId = "systemuserid";
        private const string CrmAccountManagerId = "preferredsystemuserid";
        private const string CrmOwnerId = "ownerid";
        private const string CrmFirstName = "firstname";
        private const string CrmLastName = "lastname";
        private const string CrmFullName = "fullname";
        private const string CrmTitle = "title";
        private const string CrmManagerEmail = "internalemailaddress";
        private const string Manager = "manager";
        private const string Owner = "owner";
        private const string CrmRelationshipType = "customertypecode";
        private const string CrmModifiedOn = "modifiedon";
        private const string CrmStatus = "statecode";
        private const string CrmPlatformAccess = "new_platformaccess";
        private const string CrmPlatformTier = "new_platformtier";
        private const string OfficeName = Office + "." + CrmName;
        private const string ManagerEmail = Manager + "." + CrmManagerEmail;
        private const string ManagerFirstName = Manager + "." + CrmFirstName;
        private const string ManagerLastName = Manager + "." + CrmLastName;
        private const string ManagerFullName = Manager + "." + CrmFullName;
        private const string ManagerTitle = Manager + "." + CrmTitle;
        private const string ManagerModifiedOn = Manager + "." + CrmModifiedOn;

        private const string OwnerEmail = Owner + "." + CrmManagerEmail;
        private const string OwnerFirstName = Owner + "." + CrmFirstName;
        private const string OwnerLastName = Owner + "." + CrmLastName;
        private const string OwnerFullName = Owner + "." + CrmFullName;
        private const string OwnerTitle = Owner + "." + CrmTitle;

        private const string OfficeModifiedOn = Office + "." + CrmModifiedOn;
        private const string CrmAccountId = "accountid";

        private const string CrmAFEntityName = "new_accountfeature";
        private const string CrmAFAccountId = "new_account";
        private const string CrmAFFeatureId = "new_feature";
        private const string CrmAFStatus = "statecode";

        private const string CrmFeatureEntityName = "new_feature";
        private const string CrmFeatureId = "new_featureid";
        private const string CrmFeatureStatus = "statecode";
        private const string CrmFeatureName = "new_name";
        private const string CrmFeatureParent = "new_parentfeature";

        private const string CrmFCEntityName = "new_new_feature_new_claim";
        private const string CrmFCClaimId = "new_claimid";
        private const string CrmFCFeatureId = "new_featureid";

        private const string CrmClaimEntityName = "new_claim";
        private const string CrmClaimId = "new_claimid";
        private const string CrmClaimStatus = "statecode";
        private const string CrmClaimName = "new_name";
        private const string CrmClaimValue = "new_value";

        #endregion



        private string _connectionString;

        private DynamicsCrmGateway() { }// prohibit public instantiation

        public static IDynamicsCrmGateway GetInstance(string connectionString)
        {
            if (string.IsNullOrWhiteSpace(connectionString))
            {
                throw new ArgumentException("A connection string is needed in order to create an IDynamicsCrmGateway instance!");
            }

            return new DynamicsCrmGateway { _connectionString = connectionString };
        }
        

        public TimeSpan GetCrmTimeOffset()
        {
            using (var crmService = new OrganizationService(CrmConnection.Parse(_connectionString)))
            {
                var crmEntityName = "new_serverclocksync";              
                var fakeRecord = new CrmEntity(crmEntityName) { Id = Guid.NewGuid() };
                crmService.Create(fakeRecord);

                var crmModifiedOn = (DateTime)crmService.Retrieve(fakeRecord.LogicalName, fakeRecord.Id, new ColumnSet(CrmModifiedOn))[CrmModifiedOn];

                crmService.Delete(fakeRecord.LogicalName, fakeRecord.Id);

                var savingStop = DateTime.UtcNow;
                var diff = crmModifiedOn - savingStop;
                return diff;
            }
        }       

        public List<Guid> ListAccountsIds()
        {
            using (var crmService = new OrganizationService(CrmConnection.Parse(_connectionString)))
            {
                return crmService.RetrieveMultiple(new QueryExpression(CrmAccount)).Entities.Select(a => a.Id).ToList();
            }
        }

        public List<Sustainalytics.Entities.Account> ListChangedAccounts(DateTime changedSince)
        {
            DateTime minDate = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            changedSince = changedSince < minDate ? minDate : changedSince;
            using (var crmService = new OrganizationService(CrmConnection.Parse(_connectionString)))
            {
                var changedAccounts = new QueryExpression(CrmAccount);

                var office = new LinkEntity(CrmAccount, CrmOffice, CrmOfficeId, CrmOfficeId, JoinOperator.LeftOuter);
                office.Columns.AddColumns(CrmName, CrmModifiedOn);
                office.EntityAlias = Office;

                var manager = new LinkEntity(CrmAccount, CrmManager, CrmAccountManagerId, CrmManagerId, JoinOperator.LeftOuter);
                manager.Columns.AddColumns(CrmManagerEmail, CrmFirstName, CrmLastName, CrmFullName, CrmTitle, CrmModifiedOn);
                manager.EntityAlias = Manager;

                var owner = new LinkEntity(CrmAccount, CrmManager, CrmOwnerId, CrmManagerId, JoinOperator.LeftOuter);
                owner.Columns.AddColumns(CrmManagerEmail, CrmFirstName, CrmLastName, CrmFullName, CrmTitle, CrmModifiedOn);
                owner.EntityAlias = Owner;
                
                changedAccounts.ColumnSet.AddColumns(CrmName, CrmRelationshipType, CrmModifiedOn, CrmStatus, CrmPlatformAccess, CrmPlatformTier);
                changedAccounts.LinkEntities.AddRange(new[] { office, manager, owner });

                changedAccounts.Criteria.AddCondition(CrmModifiedOn, ConditionOperator.GreaterThan, changedSince);
                changedAccounts.Criteria.FilterOperator = LogicalOperator.Or;

                var officeFilter = changedAccounts.Criteria.AddFilter(LogicalOperator.Or);
                officeFilter.AddCondition(new ConditionExpression(CrmOffice, CrmModifiedOn, ConditionOperator.GreaterThan, changedSince));

                var managerFilter = changedAccounts.Criteria.AddFilter(LogicalOperator.Or);
                managerFilter.AddCondition(new ConditionExpression(Manager, CrmModifiedOn, ConditionOperator.GreaterThan, changedSince));

                return crmService.RetrieveMultiple(changedAccounts).Entities.Select(a => new Sustainalytics.Entities.Account
                                    {
                                        Id = a.Id,
                                        Name = a.GetAttributeValue<string>(CrmName),
                                        RelationshipType = a.Contains(CrmRelationshipType) ? Enum.IsDefined(typeof(RelationshipType), a.GetAttributeValue<OptionSetValue>(CrmRelationshipType).Value) ? (RelationshipType)a.GetAttributeValue<OptionSetValue>(CrmRelationshipType).Value : RelationshipType.FormerClient
                                                                                            : RelationshipType.FormerClient,
                                        Office = a.Contains(OfficeName) ? a.GetAttributeValue<AliasedValue>(OfficeName).Value as string : null,
                                        AccountManager = a.Contains(ManagerEmail) ? new AccountManager
                                        {
                                            Email = a.GetAttributeValue<AliasedValue>(ManagerEmail).Value as string,
                                            FirstName = a.Contains(ManagerFirstName) ? a.GetAttributeValue<AliasedValue>(ManagerFirstName).Value as string : null,
                                            LastName = a.Contains(ManagerLastName) ? a.GetAttributeValue<AliasedValue>(ManagerLastName).Value as string : null,
                                            FullName = a.Contains(ManagerFullName) ? a.GetAttributeValue<AliasedValue>(ManagerFullName).Value as string : null,
                                            Title = a.Contains(ManagerTitle) ? a.GetAttributeValue<AliasedValue>(ManagerTitle).Value as string : null
                                        } : a.Contains(OwnerEmail) ? new AccountManager
                                        {
                                            Email = a.GetAttributeValue<AliasedValue>(OwnerEmail).Value as string,
                                            FirstName = a.Contains(OwnerFirstName) ? a.GetAttributeValue<AliasedValue>(OwnerFirstName).Value as string : null,
                                            LastName = a.Contains(OwnerLastName) ? a.GetAttributeValue<AliasedValue>(OwnerLastName).Value as string : null,
                                            FullName = a.Contains(OwnerFullName) ? a.GetAttributeValue<AliasedValue>(OwnerFullName).Value as string : null,
                                            Title = a.Contains(OwnerTitle) ? a.GetAttributeValue<AliasedValue>(OwnerTitle).Value as string : null
                                        } : null,
                                        ModifiedOn = GetMaxDate(a.GetAttributeValue<DateTime>(CrmModifiedOn),
                                                                a.Contains(OfficeModifiedOn) ? (DateTime)a.GetAttributeValue<AliasedValue>(OfficeModifiedOn).Value : (DateTime?)null,
                                                                a.Contains(ManagerModifiedOn) ? (DateTime)a.GetAttributeValue<AliasedValue>(ManagerModifiedOn).Value : (DateTime?)null),
                                        Status = (Status)a.GetAttributeValue<OptionSetValue>(CrmStatus).Value, 
                                        HasPlatformAccess = a.GetAttributeValue<bool>(CrmPlatformAccess), 
                                        PlatformTier = a.Contains(CrmPlatformTier) ? Enum.IsDefined(typeof(AccessLevel), a.GetAttributeValue<OptionSetValue>(CrmPlatformTier).Value) ? (AccessLevel)a.GetAttributeValue<OptionSetValue>(CrmPlatformTier).Value : AccessLevel.Tier3 
                                                                                    : AccessLevel.Tier3,
                                    })
                                    .ToList();
            }
        }

        private static DateTime GetMaxDate(DateTime accountModifiedOn, DateTime? officeModifiedOn, DateTime? managerModifiedOn)
        {
            var maxDate = accountModifiedOn;

            if (officeModifiedOn.HasValue && officeModifiedOn > maxDate)
            {
                maxDate = officeModifiedOn.Value;
        }

            if (managerModifiedOn.HasValue && managerModifiedOn > maxDate)
            {
                maxDate = managerModifiedOn.Value;
            }

            return maxDate;
        }        

        public List<Guid> ListContactsIds()
        {
            
            var query = new QueryExpression(CrmContactInfo.EntityName);
            query.Criteria.AddCondition(CrmContactInfo.AccountId, ConditionOperator.NotNull);

            var account = new LinkEntity(CrmContactInfo.EntityName, CrmAccount, CrmContactInfo.AccountId, CrmAccountId, JoinOperator.Inner);
            query.LinkEntities.Add(account);
            
            var filter = query.Criteria.AddFilter(LogicalOperator.Or);
            filter.AddCondition(new ConditionExpression(CrmAccount, CrmRelationshipType, ConditionOperator.Equal, (int)RelationshipType.Client));
            filter.AddCondition(new ConditionExpression(CrmAccount, CrmRelationshipType, ConditionOperator.Equal, (int)RelationshipType.Demo));
            filter.AddCondition(new ConditionExpression(CrmAccount, CrmRelationshipType, ConditionOperator.Equal, (int)RelationshipType.Prospect));
            

            using (var crmService = new OrganizationService(CrmConnection.Parse(_connectionString)))
            {
                EntityCollection entitiesBatch;

                List<Guid> contactIds = new List<Guid>();

                for (;;)
                {
                   entitiesBatch = crmService.RetrieveMultiple(query);

                   contactIds.AddRange(entitiesBatch.Entities.Select(contact => contact.Id));

                    if (entitiesBatch.MoreRecords)
                    {
                        query.PageInfo.PageNumber++;
                        query.PageInfo.PagingCookie = entitiesBatch.PagingCookie;
                    }
                    else
                    {
                        break;
                    }
                }

                return contactIds.Distinct().ToList();
            }
        }

        public List<CrmContact> ListChangedContacts(DateTime changedSince)
        {
            DateTime minDate = new DateTime(1900, 1, 1, 0,0,0, DateTimeKind.Utc);
            minDate = changedSince < minDate ? minDate : changedSince;
            using (var crmService = new OrganizationService(CrmConnection.Parse(_connectionString)))
            {
                var queryContacts = new QueryExpression(CrmContactInfo.EntityName);

                queryContacts.ColumnSet.Columns.AddRange(new[] {
                    CrmContactInfo.ModifiedOn,
                    CrmContactInfo.FirstName, CrmContactInfo.LastName, CrmContactInfo.MiddleName,
                    CrmContactInfo.AccountId, CrmContactInfo.Email, 
                    CrmContactInfo.StateCode, CrmContactInfo.IsPlatformUser});

                var account = new LinkEntity(CrmContactInfo.EntityName, CrmAccount,CrmContactInfo.AccountId, CrmAccountId, JoinOperator.Inner);
                queryContacts.LinkEntities.Add(account);

                queryContacts.Criteria.AddCondition(CrmContactInfo.ModifiedOn, ConditionOperator.GreaterThan, minDate);
                queryContacts.Criteria.AddCondition(CrmContactInfo.AccountId, ConditionOperator.NotNull);
                
                queryContacts.Criteria.AddCondition(new ConditionExpression(CrmAccount, CrmStatus, ConditionOperator.Equal, (int)Status.Active));                
                
                var filter = queryContacts.Criteria.AddFilter(LogicalOperator.Or);
                filter.AddCondition(new ConditionExpression(CrmAccount, CrmRelationshipType, ConditionOperator.Equal,(int) RelationshipType.Client));
                filter.AddCondition(new ConditionExpression(CrmAccount, CrmRelationshipType, ConditionOperator.Equal, (int)RelationshipType.Demo));
                filter.AddCondition(new ConditionExpression(CrmAccount, CrmRelationshipType, ConditionOperator.Equal, (int) RelationshipType.Prospect));
                
                var result =  crmService.RetrieveMultiple(queryContacts);
                var contacts = new List<CrmContact>();
                
                if(result != null && result.Entities != null)
                foreach (var entity in result.Entities)
                {
                    var contact = ToContact(entity);

                    contacts.Add(contact);
                }
                return contacts;
            }
        }

        public List<CrmContact> ListContactsByAccountId(Guid accountId)
        {
            using (var crmService = new OrganizationService(CrmConnection.Parse(_connectionString)))
            {
                var queryContacts = new QueryExpression(CrmContactInfo.EntityName);

                queryContacts.ColumnSet.Columns.AddRange(new[] {
                    CrmContactInfo.ModifiedOn,
                    CrmContactInfo.FirstName, CrmContactInfo.LastName, CrmContactInfo.MiddleName,
                    CrmContactInfo.AccountId, CrmContactInfo.Email, 
                    CrmContactInfo.StateCode, CrmContactInfo.IsPlatformUser});

                var account = new LinkEntity(CrmContactInfo.EntityName, CrmAccount, CrmContactInfo.AccountId, CrmAccountId, JoinOperator.Inner);
                queryContacts.LinkEntities.Add(account);
                queryContacts.Criteria.AddCondition(CrmContactInfo.AccountId, ConditionOperator.Equal, accountId);

                var result = crmService.RetrieveMultiple(queryContacts);
                var contacts = new List<CrmContact>();

                if (result != null && result.Entities != null)
                    foreach (var entity in result.Entities)
                    {
                        var contact = ToContact(entity);

                        contacts.Add(contact);
                    }
                return contacts;
            }
        }

        private CrmContact ToContact(Entity entity)
        {
            var contact = new CrmContact
            {
                Id = entity.Id
            };

            var accontReference = entity.GetAttributeValue("parentcustomerid") as EntityReference;
            if (accontReference != null)
            {
                contact.AccountId = accontReference.Id;
            }

            contact.Email = entity.GetAttributeValue<string>(CrmContactInfo.Email);

            contact.FirstName = entity.GetAttributeValue<string>(CrmContactInfo.FirstName);
            contact.LastName = entity.GetAttributeValue<string>(CrmContactInfo.LastName);
            contact.MiddleName = entity.GetAttributeValue<string>(CrmContactInfo.MiddleName);
            contact.FullName = entity.GetAttributeValue<string>(CrmContactInfo.FullName);
            contact.ModifiedOn = entity.GetAttributeValue<DateTime>(CrmContactInfo.ModifiedOn);
            contact.IsPlatformUser = entity.GetAttributeValue<bool>(CrmContactInfo.IsPlatformUser);
            
            contact.IsActive = false;
            var stateEntry = entity.GetAttributeValue<OptionSetValue>(CrmContactInfo.StateCode);
            if (stateEntry != null && stateEntry.Value == (int)CrmContactInfo.StateCodeEnum.Active)
            {
                contact.IsActive = true;
            }
            return contact;
        }

        public SchemaValidationResult ValidateSchema()
        {
            using (var crmService = new OrganizationService(CrmConnection.Parse(_connectionString)))
            {
                var attReq = new Microsoft.Xrm.Sdk.Messages.RetrieveAttributeRequest
                {
                    EntityLogicalName = "account",
                    LogicalName = CrmRelationshipType,
                    RetrieveAsIfPublished = true
                };

                var attResponse = (Microsoft.Xrm.Sdk.Messages.RetrieveAttributeResponse)crmService.Execute(attReq);
                var attMetadata = (Microsoft.Xrm.Sdk.Metadata.EnumAttributeMetadata)attResponse.AttributeMetadata;

                var optionList = attMetadata.OptionSet.Options;
                
                var errorList = new List<string>();

                ValidateEnumField(optionList, errorList, "Former Client", (int)RelationshipType.FormerClient);
                ValidateEnumField(optionList, errorList, "Client", (int)RelationshipType.Client);
                ValidateEnumField(optionList, errorList, "Demo", (int)RelationshipType.Demo);
                ValidateEnumField(optionList, errorList, "Prospect", (int)RelationshipType.Prospect);

                var accountValidationResults = new EntityValidationResult
                {
                    Errors = errorList,
                    EntityName = string.Format("{0}:{1}", attReq.EntityLogicalName, attReq.LogicalName),
                    IsValid = errorList.Count == 0
                };
                var entities = new List<EntityValidationResult>();
                entities.Add(accountValidationResults);
                return new SchemaValidationResult { Entities = entities, IsValid = accountValidationResults.IsValid };

            }
        }

        private static void ValidateEnumField(Microsoft.Xrm.Sdk.Metadata.OptionMetadataCollection optionList, List<string> errorList, string enumFieldName, int enumFieldValue)
        {

            var formerClient = optionList.FirstOrDefault((o) => o.Label.UserLocalizedLabel.Label == enumFieldName);
            if (formerClient == null)
            {
                errorList.Add(string.Format("missing {0}", enumFieldName));
            }
            else if (formerClient.Value != enumFieldValue)
            {
                errorList.Add(string.Format("invalid value for {0} expected\"{1}\" found \"{2}\"", enumFieldName, enumFieldValue, formerClient.Value));
            }
        }

        public IEnumerable<Feature> ListFeatures()
        {
            var queryFeatures = new QueryExpression(CrmFeatureEntityName);
            queryFeatures.ColumnSet.Columns.AddRange(new[] { CrmFeatureId, CrmFeatureName, CrmFeatureParent });
            queryFeatures.Criteria.AddCondition(new ConditionExpression(CrmFeatureEntityName, CrmFeatureStatus, ConditionOperator.Equal, (int)Status.Active));
            
            return ListEntity<Feature>(queryFeatures);
        }

        public IEnumerable<Claim> ListClaims()
        {
            var queryClaims = new QueryExpression(CrmClaimEntityName);
            queryClaims.ColumnSet.Columns.AddRange(new[] { CrmClaimId, CrmClaimName, CrmClaimValue });
            queryClaims.Criteria.AddCondition(new ConditionExpression(CrmClaimEntityName, CrmClaimStatus, ConditionOperator.Equal, (int)Status.Active));
            return ListEntity<Claim>(queryClaims);
        }

        public IEnumerable<FeatureClaimBridge> ListFeatureClaims()
        {
            var queryFeatures = new QueryExpression(CrmFCEntityName);
            queryFeatures.ColumnSet.Columns.AddRange(new[] { CrmFCClaimId, CrmFCFeatureId });
            return ListEntity<FeatureClaimBridge>(queryFeatures);
        }

        public IEnumerable<AccountFeature> ListAccountFeatures()
        {
            var query = new QueryExpression(CrmAFEntityName);
            query.ColumnSet.Columns.AddRange(new[] { CrmAFAccountId, CrmAFFeatureId });
            query.Criteria.AddCondition(new ConditionExpression(CrmAFEntityName, CrmAFStatus, ConditionOperator.Equal, (int)Status.Active));
            return ListEntity<AccountFeature>(query);
        }

        private IEnumerable<T> ListEntity<T>(QueryExpression query) where T : Microsoft.Xrm.Sdk.Entity
        {
            using (var crmService = new OrganizationService(CrmConnection.Parse(_connectionString)))
            {
                var results = new List<T>();

                for (; ; )
                {
                    var entitiesBatch = crmService.RetrieveMultiple(query);

                    results.AddRange(entitiesBatch.Entities.Cast<T>());

                    if (entitiesBatch.MoreRecords)
                    {
                        query.PageInfo.PageNumber++;
                        query.PageInfo.PagingCookie = entitiesBatch.PagingCookie;
                    }
                    else
                    {
                        break;
                    }
                }

                return results;
            }
        }
    }
}
